Supabase: The Backend for Vibe Coders

Last updated: September 10 2025

Overview

So, you want to build something cool but the backend stuff feels like a total drag. Databases, authentication, servers... it's a lot. What if you could get all that sorted out with a few clicks and a bit of code?

Enter Supabase. It's an open-source platform that gives you a full backend – a database, user authentication, file storage, and more – without you having to be a backend genius. It’s perfect for vibe coding because it lets you focus on what actually matters: building your app and getting your ideas out there, fast.

This tutorial will give you the lowdown on the essential Supabase features. We'll get you set up, teach you how to save and retrieve data, handle user sign-ups, and even store files.

Let's get into the vibe 🤗

Step 1: Get Your Supabase Project Started

First things first, you need a Supabase account and a project.

  1. Go to supabase.com and sign up. You can use your GitHub account to make it super quick.
  2. Once you're in, create a new organization, and create a "New Project".
  3. Give your project a name, generate a secure database password (and save it somewhere safe like a password manager!), and choose a region close to you.
  4. Your backend is being built!

When it's ready, navigate to your project dashboard -> Project Settings. We'll get some important environment variables for your app:

  • Find your Project URL under Data API section.
  • Find your Publishable key and Secret key in API Keys section.

Keep this tab open. You'll need to copy these keys into your app soon.

Step 2: Create the todos table

Use the Table Editor or run the SQL below.

sql
create table if not exists public.todos (
  id bigint generated by default as identity primary key,
  user_id uuid not null references auth.users (id) on delete cascade,
  title text not null,
  is_complete boolean not null default false,
  inserted_at timestamp with time zone default now()
);

alter table public.todos enable row level security;

Step 3: Initialize Supabase in your app

Install the client:

bash
npm install @supabase/supabase-js

Create a client file for the browser (safe with RLS + policies):

javascript
// lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'YOUR_SUPABASE_PROJECT_URL'
const supabaseKey = 'YOUR_SUPABASE_PUBLISHABLE_KEY' // formerly "anon" key

export const supabase = createClient(supabaseUrl, supabaseKey)

// Tip: prefer env vars (Astro/Next/etc.)
// const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL
// const supabaseKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY

If you also have server code (API routes, CRON, webhooks), use the Secret key there only:

javascript
// server/supabaseServer.js (Node/Edge runtime)
import { createClient } from '@supabase/supabase-js'

export const supabaseServer = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SECRET_KEY // never expose to the client
)

Step 4: Auth — Sign up and sign in

javascript
// Sign up
async function signUpUser(email, password) {
  const { data, error } = await supabase.auth.signUp({ email, password })
  if (error) { console.error('Error signing up:', error.message); return null }
  return data.user
}

// Sign in
async function signInUser(email, password) {
  const { data, error } = await supabase.auth.signInWithPassword({ email, password })
  if (error) { console.error('Error signing in:', error.message); return null }
  return data.user
}

Step 5: CRUD — Add, list, toggle, and delete todos

javascript
// Add a todo (requires authenticated user)
async function addTodo(title) {
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not signed in')
  const { data, error } = await supabase
    .from('todos')
    .insert([{ title, user_id: user.id }])
    .select()
  if (error) { console.error('Error adding todo:', error.message); return null }
  return data
}

// List todos (most recent first)
async function fetchTodos() {
  const { data, error } = await supabase
    .from('todos')
    .select('*')
    .order('inserted_at', { ascending: false })
  if (error) { console.error('Error fetching todos:', error.message); return [] }
  return data
}

// Toggle completion
async function toggleTodo(todoId, nextComplete) {
  const { data, error } = await supabase
    .from('todos')
    .update({ is_complete: nextComplete })
    .eq('id', todoId)
    .select()
  if (error) { console.error('Error updating todo:', error.message); return null }
  return data
}

// Delete a todo
async function deleteTodo(todoId) {
  const { error } = await supabase
    .from('todos')
    .delete()
    .eq('id', todoId)
  if (error) { console.error('Error deleting todo:', error.message) }
}

Step 6: Secure with RLS (policies)

Your Publishable key is client-safe only when Row Level Security (RLS) is enabled and policies are correct. Lock the table to row owners:

sql
create policy "Select own todos" on public.todos
for select using (auth.uid() = user_id);

create policy "Insert own todos" on public.todos
for insert with check (auth.uid() = user_id);

create policy "Update own todos" on public.todos
for update using (auth.uid() = user_id);

create policy "Delete own todos" on public.todos
for delete using (auth.uid() = user_id);

Optional: Realtime updates

javascript
const { data: { user } } = await supabase.auth.getUser()

const channel = supabase
  .channel('todos-changes')
  .on('postgres_changes',
    { event: '*', schema: 'public', table: 'todos', filter: `user_id=eq.${user.id}` },
    payload => {
      console.log('Change received!', payload)
      // e.g., re-fetch or update local state
    })
  .subscribe()

Wrap-up

You built a secure Todo backend with Supabase:

  • Created a todos table
  • Wired up client with the Publishable key
  • Implemented auth and CRUD
  • Secured data with RLS policies
  • (Optional) Realtime updates

Now plug this into your UI framework of choice and ship it. Vibe on.